{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:37.505092Z",
     "start_time": "2025-01-20T06:39:28.540647Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:23.690453Z",
     "iopub.status.busy": "2025-01-21T11:06:23.690143Z",
     "iopub.status.idle": "2025-01-21T11:06:25.646618Z",
     "shell.execute_reply": "2025-01-21T11:06:25.646144Z",
     "shell.execute_reply.started": "2025-01-21T11:06:23.690425Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=10, micro=14, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cu124\n",
      "cuda:0\n"
     ]
    }
   ],
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.752355Z",
     "start_time": "2025-01-20T06:39:37.505092Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:34.584337Z",
     "iopub.status.busy": "2025-01-21T11:06:34.583914Z",
     "iopub.status.idle": "2025-01-21T11:06:35.463725Z",
     "shell.execute_reply": "2025-01-21T11:06:35.463165Z",
     "shell.execute_reply.started": "2025-01-21T11:06:34.584314Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "load 1097 images from training dataset\n",
      "load 272 images from validation dataset\n"
     ]
    }
   ],
   "source": [
    "from torchvision import datasets\n",
    "from torchvision.transforms import ToTensor, Resize, Compose, ConvertImageDtype, Normalize\n",
    "\n",
    "\n",
    "from pathlib import Path\n",
    "\n",
    "DATA_DIR1 = Path(\"/content/training/\")\n",
    "DATA_DIR2 = Path(\"/content/validation/\")\n",
    "\n",
    "class MonkeyDataset(datasets.ImageFolder):\n",
    "    def __init__(self, mode, transform=None):\n",
    "        if mode == \"train\":\n",
    "            root = DATA_DIR1 / \"training\"\n",
    "        elif mode == \"val\":\n",
    "            root = DATA_DIR2 / \"validation\"\n",
    "        else:\n",
    "            raise ValueError(\"mode should be one of the following: train, val, but got {}\".format(mode))\n",
    "        super().__init__(root, transform)\n",
    "        self.imgs = self.samples\n",
    "        self.targets = [s[1] for s in self.samples]\n",
    "\n",
    "# resnet 要求的，见 https://pytorch.org/vision/stable/models/generated/torchvision.models.resnet50.html\n",
    "img_h, img_w = 224, 224\n",
    "transform = Compose([\n",
    "     Resize((img_h, img_w)),\n",
    "     ToTensor(),\n",
    "     Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n",
    "     ConvertImageDtype(torch.float),\n",
    "])\n",
    "\n",
    "\n",
    "train_ds = MonkeyDataset(\"train\", transform=transform)\n",
    "val_ds = MonkeyDataset(\"val\", transform=transform)\n",
    "\n",
    "print(\"load {} images from training dataset\".format(len(train_ds)))\n",
    "print(\"load {} images from validation dataset\".format(len(val_ds)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.756387Z",
     "start_time": "2025-01-20T06:39:38.752355Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:35.464807Z",
     "iopub.status.busy": "2025-01-21T11:06:35.464542Z",
     "iopub.status.idle": "2025-01-21T11:06:35.468959Z",
     "shell.execute_reply": "2025-01-21T11:06:35.468453Z",
     "shell.execute_reply.started": "2025-01-21T11:06:35.464790Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9'],\n",
       " {'n0': 0,\n",
       "  'n1': 1,\n",
       "  'n2': 2,\n",
       "  'n3': 3,\n",
       "  'n4': 4,\n",
       "  'n5': 5,\n",
       "  'n6': 6,\n",
       "  'n7': 7,\n",
       "  'n8': 8,\n",
       "  'n9': 9})"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 数据类别\n",
    "train_ds.classes, train_ds.class_to_idx"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.775815Z",
     "start_time": "2025-01-20T06:39:38.756387Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:35.469762Z",
     "iopub.status.busy": "2025-01-21T11:06:35.469518Z",
     "iopub.status.idle": "2025-01-21T11:06:35.479283Z",
     "shell.execute_reply": "2025-01-21T11:06:35.478754Z",
     "shell.execute_reply.started": "2025-01-21T11:06:35.469746Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/content/training/training/n0/n0018.jpg 0\n",
      "torch.Size([3, 224, 224]) 0\n"
     ]
    }
   ],
   "source": [
    "# 图片路径 及 标签\n",
    "for fpath, label in train_ds.imgs:\n",
    "    print(fpath, label)\n",
    "    break\n",
    "\n",
    "for img, label in train_ds:\n",
    "    # c, h, w  label\n",
    "    print(img.shape, label)\n",
    "    break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.778866Z",
     "start_time": "2025-01-20T06:39:38.775815Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:35.480958Z",
     "iopub.status.busy": "2025-01-21T11:06:35.480509Z",
     "iopub.status.idle": "2025-01-21T11:06:35.484222Z",
     "shell.execute_reply": "2025-01-21T11:06:35.483778Z",
     "shell.execute_reply.started": "2025-01-21T11:06:35.480937Z"
    }
   },
   "outputs": [],
   "source": [
    "# 遍历train_ds得到每张图片，计算每个通道的均值和方差\n",
    "def cal_mean_std(ds):\n",
    "    mean = 0.\n",
    "    std = 0.\n",
    "    for img, _ in ds:\n",
    "        mean += img.mean(dim=(1, 2))\n",
    "        std += img.std(dim=(1, 2))\n",
    "    mean /= len(ds)\n",
    "    std /= len(ds)\n",
    "    return mean, std\n",
    "\n",
    "# 经过 normalize 后 均值为0，方差为1\n",
    "# print(cal_mean_std(train_ds))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.781799Z",
     "start_time": "2025-01-20T06:39:38.778866Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:35.484810Z",
     "iopub.status.busy": "2025-01-21T11:06:35.484663Z",
     "iopub.status.idle": "2025-01-21T11:06:35.487681Z",
     "shell.execute_reply": "2025-01-21T11:06:35.487259Z",
     "shell.execute_reply.started": "2025-01-21T11:06:35.484795Z"
    }
   },
   "outputs": [],
   "source": [
    "import torch.nn as nn\n",
    "from torch.utils.data.dataloader import DataLoader    \n",
    "\n",
    "batch_size = 16\n",
    "# 从数据集到dataloader\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:19:28.364598500Z",
     "start_time": "2024-07-23T02:19:27.176829700Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:35.488389Z",
     "iopub.status.busy": "2025-01-21T11:06:35.488087Z",
     "iopub.status.idle": "2025-01-21T11:06:35.700830Z",
     "shell.execute_reply": "2025-01-21T11:06:35.700185Z",
     "shell.execute_reply.started": "2025-01-21T11:06:35.488375Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([16, 3, 224, 224])\n",
      "torch.Size([16])\n"
     ]
    }
   ],
   "source": [
    "for imgs, labels in train_loader:\n",
    "    print(imgs.shape)\n",
    "    print(labels.shape)\n",
    "    break"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T07:28:55.526142Z",
     "start_time": "2025-01-20T07:28:55.358597Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:35.701620Z",
     "iopub.status.busy": "2025-01-21T11:06:35.701431Z",
     "iopub.status.idle": "2025-01-21T11:06:35.707343Z",
     "shell.execute_reply": "2025-01-21T11:06:35.706753Z",
     "shell.execute_reply.started": "2025-01-21T11:06:35.701603Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "from torchvision.models import resnet50\n",
    "\n",
    "\n",
    "# 加载resnet50模型以及预训练权重，冻结除最后一层外所有层的权重，去除最后一层并添加自定义分类层\n",
    "class ResNet50(nn.Module):\n",
    "    def __init__(self, num_classes=10, frozen=True):\n",
    "        super().__init__()\n",
    "        # 下载预训练权重\n",
    "        self.model = resnet50(weights=\"IMAGENET1K_V2\",) # 这里的weights参数可以选择\"IMAGENET1K_V2\"或None，None表示随机初始化\n",
    "        # 冻结前面的层\n",
    "        if frozen:\n",
    "            for param in self.model.parameters():\n",
    "                param.requires_grad = False # 冻结权重\n",
    "        # for param in self.model.layer4.parameters():\n",
    "        #     param.requires_grad = True  # 解冻 layer4\n",
    "        # for name, param in self.model.named_parameters():\n",
    "        #     if name == \"layer4.2.conv3.weight\":\n",
    "        #         param.requires_grad = True  # 解冻该层\n",
    "        # 添加自定义分类层\n",
    "        # print(self.model)\n",
    "        print(self.model.fc.in_features) # 打印resnet50的最后一层的输入通道数\n",
    "        print(self.model.fc.out_features) # 打印resnet50的最后一层的输出通道数 1000\n",
    "        self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)   # 自定义分类层,把resnet50的最后一层改为num_classes个输出\n",
    "        \n",
    "        \n",
    "    def forward(self, x):\n",
    "        return self.model(x)\n",
    "\n",
    "\n",
    "# for idx, (key, value) in enumerate(ResNet50().named_parameters()):\n",
    "#     print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T07:28:58.912249Z",
     "start_time": "2025-01-20T07:28:58.749737Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:35.708392Z",
     "iopub.status.busy": "2025-01-21T11:06:35.708012Z",
     "iopub.status.idle": "2025-01-21T11:06:41.976075Z",
     "shell.execute_reply": "2025-01-21T11:06:41.975480Z",
     "shell.execute_reply.started": "2025-01-21T11:06:35.708364Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Downloading: \"https://download.pytorch.org/models/resnet50-11ad3fa6.pth\" to /root/.cache/torch/hub/checkpoints/resnet50-11ad3fa6.pth\n",
      "100%|██████████| 97.8M/97.8M [00:04<00:00, 20.5MB/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "20490"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model = ResNet50(num_classes=10, frozen=True)\n",
    "def count_parameters(model): #计算模型总参数量\n",
    "    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "count_parameters(model)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T07:08:10.086234Z",
     "start_time": "2025-01-20T07:08:10.083273Z"
    }
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:48:29.601561Z",
     "start_time": "2025-01-20T06:48:29.597356Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:41.977030Z",
     "iopub.status.busy": "2025-01-21T11:06:41.976703Z",
     "iopub.status.idle": "2025-01-21T11:06:41.981768Z",
     "shell.execute_reply": "2025-01-21T11:06:41.981196Z",
     "shell.execute_reply.started": "2025-01-21T11:06:41.977013Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ResNet50(\n",
       "  (model): ResNet(\n",
       "    (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n",
       "    (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    (relu): ReLU(inplace=True)\n",
       "    (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n",
       "    (layer1): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "          (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer2): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer3): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (4): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (5): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer4): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))\n",
       "    (fc): Linear(in_features=2048, out_features=10, bias=True)\n",
       "  )\n",
       ")"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:48:57.859769Z",
     "start_time": "2025-01-20T06:48:57.855987Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:41.983508Z",
     "iopub.status.busy": "2025-01-21T11:06:41.983237Z",
     "iopub.status.idle": "2025-01-21T11:06:41.986934Z",
     "shell.execute_reply": "2025-01-21T11:06:41.986345Z",
     "shell.execute_reply.started": "2025-01-21T11:06:41.983493Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 23528522\n"
     ]
    }
   ],
   "source": [
    "total_params = sum(p.numel() for p in model.parameters() )\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:52:19.752248Z",
     "start_time": "2025-01-20T06:52:19.747379Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:41.987524Z",
     "iopub.status.busy": "2025-01-21T11:06:41.987372Z",
     "iopub.status.idle": "2025-01-21T11:06:41.992092Z",
     "shell.execute_reply": "2025-01-21T11:06:41.991674Z",
     "shell.execute_reply.started": "2025-01-21T11:06:41.987509Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 2048, 1, 1])"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "m = nn.AdaptiveAvgPool2d(output_size=(1, 1))\n",
    "input = torch.randn(1, 2048, 9, 9)\n",
    "output = m(input)\n",
    "output.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T07:06:55.092794100Z",
     "start_time": "2024-07-23T07:06:55.088043800Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:41.992619Z",
     "iopub.status.busy": "2025-01-21T11:06:41.992482Z",
     "iopub.status.idle": "2025-01-21T11:06:41.995623Z",
     "shell.execute_reply": "2025-01-21T11:06:41.995203Z",
     "shell.execute_reply.started": "2025-01-21T11:06:41.992605Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2359296"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512*3*3*512"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T03:45:55.270615100Z",
     "start_time": "2024-07-23T03:45:55.261439200Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:41.996311Z",
     "iopub.status.busy": "2025-01-21T11:06:41.996071Z",
     "iopub.status.idle": "2025-01-21T11:06:41.999050Z",
     "shell.execute_reply": "2025-01-21T11:06:41.998648Z",
     "shell.execute_reply.started": "2025-01-21T11:06:41.996297Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1048576"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512*1*1*2048"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:56:07.349270600Z",
     "start_time": "2024-07-23T02:56:07.337722300Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:41.999811Z",
     "iopub.status.busy": "2025-01-21T11:06:41.999550Z",
     "iopub.status.idle": "2025-01-21T11:06:42.002615Z",
     "shell.execute_reply": "2025-01-21T11:06:42.002212Z",
     "shell.execute_reply.started": "2025-01-21T11:06:41.999795Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "32.0"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512/16"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "pytorch的训练需要自行实现，包括\n",
    "1. 定义损失函数\n",
    "2. 定义优化器\n",
    "3. 定义训练步\n",
    "4. 训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:59:12.386898800Z",
     "start_time": "2024-07-23T02:59:11.605525600Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:42.003262Z",
     "iopub.status.busy": "2025-01-21T11:06:42.003078Z",
     "iopub.status.idle": "2025-01-21T11:06:42.026514Z",
     "shell.execute_reply": "2025-01-21T11:06:42.026065Z",
     "shell.execute_reply.started": "2025-01-21T11:06:42.003248Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "        preds = logits.argmax(axis=-1)    # 验证集预测\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "        \n",
    "    acc = accuracy_score(label_list, pred_list)\n",
    "    return np.mean(loss_list), acc\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TensorBoard 可视化\n",
    "\n",
    "\n",
    "训练过程中可以使用如下命令启动tensorboard服务。\n",
    "\n",
    "```shell\n",
    "tensorboard \\\n",
    "    --logdir=runs \\     # log 存放路径\n",
    "    --host 0.0.0.0 \\    # ip\n",
    "    --port 8848         # 端口\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:42.027433Z",
     "iopub.status.busy": "2025-01-21T11:06:42.027022Z",
     "iopub.status.idle": "2025-01-21T11:06:42.079999Z",
     "shell.execute_reply": "2025-01-21T11:06:42.079555Z",
     "shell.execute_reply.started": "2025-01-21T11:06:42.027410Z"
    }
   },
   "outputs": [],
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "\n",
    "class TensorBoardCallback:\n",
    "    def __init__(self, log_dir, flush_secs=10):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            log_dir (str): dir to write log.\n",
    "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
    "        \"\"\"\n",
    "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
    "\n",
    "    def draw_model(self, model, input_shape):\n",
    "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
    "        \n",
    "    def add_loss_scalars(self, step, loss, val_loss):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/loss\", \n",
    "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
    "            global_step=step,\n",
    "            )\n",
    "        \n",
    "    def add_acc_scalars(self, step, acc, val_acc):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/accuracy\",\n",
    "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
    "            global_step=step,\n",
    "        )\n",
    "        \n",
    "    def add_lr_scalars(self, step, learning_rate):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/learning_rate\",\n",
    "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
    "            global_step=step,\n",
    "            \n",
    "        )\n",
    "    \n",
    "    def __call__(self, step, **kwargs):\n",
    "        # add loss\n",
    "        loss = kwargs.pop(\"loss\", None)\n",
    "        val_loss = kwargs.pop(\"val_loss\", None)\n",
    "        if loss is not None and val_loss is not None:\n",
    "            self.add_loss_scalars(step, loss, val_loss)\n",
    "        # add acc\n",
    "        acc = kwargs.pop(\"acc\", None)\n",
    "        val_acc = kwargs.pop(\"val_acc\", None)\n",
    "        if acc is not None and val_acc is not None:\n",
    "            self.add_acc_scalars(step, acc, val_acc)\n",
    "        # add lr\n",
    "        learning_rate = kwargs.pop(\"lr\", None)\n",
    "        if learning_rate is not None:\n",
    "            self.add_lr_scalars(step, learning_rate)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Save Best\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:59:18.296416Z",
     "start_time": "2024-07-23T02:59:18.287854800Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:42.080948Z",
     "iopub.status.busy": "2025-01-21T11:06:42.080543Z",
     "iopub.status.idle": "2025-01-21T11:06:42.085250Z",
     "shell.execute_reply": "2025-01-21T11:06:42.084710Z",
     "shell.execute_reply.started": "2025-01-21T11:06:42.080930Z"
    }
   },
   "outputs": [],
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch. \n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = -1\n",
    "        \n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "        \n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "        \n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Early Stop"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:59:21.080731100Z",
     "start_time": "2024-07-23T02:59:21.073276Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:42.085998Z",
     "iopub.status.busy": "2025-01-21T11:06:42.085789Z",
     "iopub.status.idle": "2025-01-21T11:06:42.089310Z",
     "shell.execute_reply": "2025-01-21T11:06:42.088941Z",
     "shell.execute_reply.started": "2025-01-21T11:06:42.085982Z"
    }
   },
   "outputs": [],
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T03:00:28.829465300Z",
     "start_time": "2024-07-23T02:59:52.601793600Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:06:42.090065Z",
     "iopub.status.busy": "2025-01-21T11:06:42.089832Z",
     "iopub.status.idle": "2025-01-21T11:09:25.890934Z",
     "shell.execute_reply": "2025-01-21T11:09:25.890484Z",
     "shell.execute_reply.started": "2025-01-21T11:06:42.090049Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 35%|███▌      | 483/1380 [02:43<05:03,  2.96it/s, epoch=6]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 7 / global_step 483\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "                preds = logits.argmax(axis=-1)\n",
    "            \n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())    \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"acc\": acc, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss, val_acc = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"acc\": val_acc, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "                    \n",
    "                    # 1. 使用 tensorboard 可视化\n",
    "                    if tensorboard_callback is not None:\n",
    "                        tensorboard_callback(\n",
    "                            global_step, \n",
    "                            loss=loss, val_loss=val_loss,\n",
    "                            acc=acc, val_acc=val_acc,\n",
    "                            lr=optimizer.param_groups[0][\"lr\"],\n",
    "                            )\n",
    "                \n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)\n",
    "\n",
    "                    # 3. 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(val_acc)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 20\n",
    "\n",
    "model = ResNet50(num_classes=10)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "# 2. 定义优化器 采用 sgd\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.0)\n",
    "\n",
    "# 1. tensorboard 可视化\n",
    "if not os.path.exists(\"runs\"):\n",
    "    os.mkdir(\"runs\")\n",
    "# tensorboard_callback = TensorBoardCallback(\"runs/monkeys-resnet50\")\n",
    "# tensorboard_callback.draw_model(model, [1, 3, img_h, img_w])\n",
    "# 2. save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(\"checkpoints/monkeys-resnet50\", save_step=len(train_loader), save_best_only=True)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-01-21T11:09:59.043864Z",
     "iopub.status.busy": "2025-01-21T11:09:59.043567Z",
     "iopub.status.idle": "2025-01-21T11:09:59.215173Z",
     "shell.execute_reply": "2025-01-21T11:09:59.214727Z",
     "shell.execute_reply.started": "2025-01-21T11:09:59.043844Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    fig_num = len(train_df.columns)\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):    \n",
    "        axs[idx].plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        axs[idx].grid()\n",
    "        axs[idx].legend()\n",
    "        # axs[idx].set_xticks(range(0, train_df.index[-1], 5000))\n",
    "        # axs[idx].set_xticklabels(map(lambda x: f\"{int(x/1000)}k\", range(0, train_df.index[-1], 5000)))\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "    \n",
    "    plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=10)  #横坐标是 steps"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T11:10:01.589431Z",
     "iopub.status.busy": "2025-01-21T11:10:01.589087Z",
     "iopub.status.idle": "2025-01-21T11:10:06.467380Z",
     "shell.execute_reply": "2025-01-21T11:10:06.466929Z",
     "shell.execute_reply.started": "2025-01-21T11:10:01.589410Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.3507\n",
      "accuracy: 0.9853\n"
     ]
    }
   ],
   "source": [
    "# dataload for evaluating\n",
    "\n",
    "# load checkpoints\n",
    "model.load_state_dict(torch.load(\"checkpoints/monkeys-resnet50/best.ckpt\",weights_only=True, map_location=\"cpu\"))\n",
    "\n",
    "model.eval()\n",
    "loss, acc = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\\naccuracy: {acc:.4f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
